大家好,今天我們要完成主頁的新聞瀏覽頁面,並加上無限捲軸的實作。畫面會如下圖:
不過為什麼我們需要無限捲軸呢?試想一下,假設我們有 100 篇的新聞資料,這些新聞資料皆有各自的標題、圖片、文章等內容,如果在一次 api 中全數回傳,必然導致等待的時間提升,使用者體驗也會隨之下降。
這時候就需要將這 100 篇資料進行分頁處理,當看完第一頁的內容,真的有需要時再去跟 api 要第二頁的資料。減少了 api 的負擔,也控制住了等待時間。
無限捲軸可以讓這個獲取換頁內容的行為變得順暢,相當適合用在瀏覽資料上,因此我們今天會教大家如何將無限捲軸的概念實現於應用程式中。
不過要先請各位更新一下用於模擬後端資料的 db.json
檔案,我有進行更新。如果你有將 json server 部署至 vercel 上的,記得要將新的 db.json
上傳至 github,vercel 會自動幫你部署到最新版本;本地環境的話則記得要重啟 json server
網址:https://github.com/ChungHanLin/news_server_api/blob/main/db.json
在開始實作今天的內容前,需要請各位調整一下我們專案程式碼架構。
請開啟 tab_layout.dart
,原先在建構頁面的 tabBuilder
中我們將頁面的外框骨架(CupertinoPageScaffold
) 與頂端列(CupertinoSliverNavigationBar
) 給四個 tab 共用,並靠著 getTabScreen()
函式來決定 tab 分頁需顯示的內容。
現在需要調整成將外框骨架及頂端列的部分整併至四個分頁中,詳細原因我們在今天的文章中會進行說明,不過簡單來說是因為要實現無限捲軸的功能才必須作出以下調整。舉 home_screen.dart
檔案為例,可參考下方程式碼:
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoPageScaffold(
child: CustomScrollView(slivers: [
CupertinoSliverNavigationBar(
largeTitle: Text('主頁'),
backgroundColor: CupertinoColors.white),
SliverToBoxAdapter(child: Text('Home Screen'))
]));
}
}
此時 tab_layout.dart
檔案的 tabBuilder
便可以簡化成如下:
tabBuilder: (BuildContext context, int index) => getTabScreen(index)
API 接口 - http://localhost:3000/posts?q=[搜尋字串]&_page=[頁碼]&_limit=[每頁資料筆數]
由於皆為測試資料,因此你會發現 api 接口與搜尋 api 相當,差別僅在於是否給定搜尋字串。因此可以針對 NewsPostRepository
中的 getPosts
進行改寫
Future<List<NewsPost>> getPosts({String query = '', int page = 1, int limit = 20}) async {
try {
final response = await http.get(Uri.parse('http://localhost:3000/posts?q=$query&_page=$page&_limit=$limit'));
if (response.statusCode == 200) {
final List<dynamic> posts = jsonDecode(response.body);
return posts.map((post) => NewsPost.fromJson(post)).toList();
}
throw Exception('取得失敗');
} catch (e) {
return Future.error('連線錯誤');
}
}
我們先來了解無限捲軸的流程是如何實現的:
上述步驟是無限捲軸的流程,因此我們需要定義幾個變數。請開啟 home_screen.dart
,並修改成 stateful widget。
class _HomeScreenState extends State<HomeScreen> {
int page = 1; // 記錄當前頁碼
int limit = 10; // 一頁中有多少筆資料
bool isBottom = false; // 記錄是否已經沒有更多新聞
late List<NewsPost> _posts = []; // 存放新聞資料
final ScrollController _scrollController = ScrollController(); // 用於監聽滑動的狀態
// 以下省略
這裡出現了一個新面孔,我們來進行介紹
ScrollController
在flutter 中適用於控制可滾動 widget 的重要工具,可監聽滾動事件、控制滾動位置等等,因此在這個章節中被我們用於監聽是否滑動至該頁的底部,若是的話則繼續獲取下頁的新聞資訊。
首先需註冊監聽器,於 initState
階段中進行註冊。
@override
void initState() {
super.initState();
_scrollController.addListener(() { ... 要監聽的內容 });
}
由於是用於可滾動組件的控制器,因此當我們去檢視如:ListView
、GridView
或是 CustomScrollView
等組件的類別定義,都會看到其中有一參數為 controller
可用於填入控制器,並監聽其滾動的行為。
ListView(
controller: _scrollController,
children: [ ... ]
);
最後當不需要該控制器時,需註銷監聽的行為
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
在監聽的過程中,ScrollController
中的 position
屬性可以提供當下滾動的位置以及狀態等資訊,以下是一些常用的屬性:
position.pixels
:表示當前滾動位置position.extentInside
:顯示於螢幕中的區塊長度position.extentBefore
:未出現於螢幕中,於螢幕之上的區塊長度position.extentAfter
:未出現於螢幕中,於螢幕之下的區塊長度position.maxScrollExtent
:最大可滾動的長度當我們認識了 scrollController 之後,知道將會被用於監聽滾動的進度,藉以達成無限捲軸的效果。那麼這跟一開始改程式碼架構之間的關係呢?
由於我們在建構每個 tab 的畫面時,都是使用
CustomScrollView(
children: [
CupertinoSliverNavigationBar( ... ),
// 其他 Sliver 組件
]
)
這樣的架構來建構畫面,為要達成捲動內容時,縮小/放大頂端導覽列的效果。CustomScrollView
本身是滾動組件的一員,因此可直接用於註冊 ScrollController 來監聽滾動進度。
你可能會說,那這跟我在導覽列下方註冊一個 ListView
再加上 ScrollController 也可以達成一樣效果。但別忘了 ListView
的滾動是之於其本身,並不會牽動頂端列的滾動效果,下面我們放上比較圖就能明白了:
因為我們要將 controller 寫在 CustomScrollView
中,也就因此整體的架構需要大挪移拉~
現在我們了解了如何運用 ScrollController 了,把他加進我們的程式碼吧:
@override
void initState() {
super.initState();
// 註冊滾動監聽器
_scrollController.addListener(() {
// 當兩個條件成立時,才執行 fetchMore 函式來獲取更多新聞
// 1. 當目前滾動位置已經達到最大可滾動範圍
// 2. 尚未滑至最底部,表示還有更多新聞時
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !isBottom) {
fetchMore();
}
});
// 一開始先取得第一頁的新聞資訊
NewsPostRepository().getPosts(page: page, limit: limit).then((value) {
setState(() {
// 若回傳的新聞列表長度已經比單頁的新聞比數,表示已經沒有下一頁了
value.length < limit ? isBottom = true : isBottom = false;
_posts = value;
});
});
}
@override
void dispose() {
super.dispose();
// 別忘了要註銷監聽器
_scrollController.dispose();
}
接下來是實現 fetchMore()
函式,繼續取得下一頁的資料,並且串接至原先新聞列表的後方。
Future<void> fetchMore() async {
late List<NewsPost> morePosts;
try {
morePosts = await NewsPostRepository().getPosts(page: page + 1, limit: limit);
} catch (e) {
// 避免呼叫 api 時發生錯誤導致整個列表崩潰,因此當發生錯誤時回傳空列表
morePosts = [];
}
setState(() {
page++; // 遞增當前頁碼
_posts.addAll(morePosts); // 將獲取的更多新聞串接至元新聞列表後方
morePosts.length < limit ? isBottom = true : isBottom = false; // 同樣判斷回傳新聞長度
});
}
CustomScrollView
現在我們進入到 build
函式中,我們使用 CustomScrollView
使的整個畫面可以進行滾動,而這些滾動效果都是透過 sliver
類型的組產生的。至於 sliver 是什麼,我們前面有提過,可以參考 Day15 的文章。
因此當我們要放置一個組件至 CustomScrollView
的 slivers
參數中時,必須要注意該組件要是 sliver
類型的。以下整理滾動式組件對應的 sliver 滾動組件
功能 | 滾動組件 | Sliver 滾動組件 |
---|---|---|
列表 | ListView | SliverList |
網格 | GridView | SliverGrid |
填滿螢幕,可容納多組件 | PageView | SliverFillViewPort |
另外還有常用的
組件名稱 | 功能 |
---|---|
SliverAppBar | 顯示可伸縮的 AppBar |
SliverToBoxAdapter | 將組件轉換成 sliver 形式 |
SliverFillRemaining | 用於填充剩餘滾動空間 |
因此當我們要列表顯示新聞卡片時,原先是使用 ListView.builder
來建構,到這裡就可以直接使用 SliverList.builder
拉~
CustomScrollView(
controller: _scrollController,
slivers: [
CupertinoSliverNavigationBar(...省略),
_posts.isNotEmpty ?
// _posts 已有資料,開始建構新聞卡片
SliverList.builder() :
// _posts 尚無資料,置中顯示讀取動畫。但因其不為 sliver 類型的,因此使用 SliverToBoxAdpater 包住
const SliverToBoxAdapter(
child: Center(child: CupertinoActivityIndicator())
)
],
剩下最後一步拉,我們要實現新聞卡片的顯示邏輯
SliverList.builder(
// 這個 + 1 相當重要,除了原先要顯示的新聞內容外,多出來的 1 個 item 將用於顯示有用的資訊
itemCount: _posts.length + 1,
itemBuilder: (conext, index) {
// 一般 item 皆為顯示新聞卡片
if (index < _posts.length) {
return NewsPostCard(post: _posts[index]);
} else if (isBottom) {
// 若已經到底了,則多出來的 item 用於顯示提示文字
return const SizedBox(
height: 36,
child: Center(
child: Text(
'已經到底拉',
style: TextStyle(color: CupertinoColors.systemGrey),
)));
} else {
// 表示尚未到底,則顯示讀取動畫
return const SizedBox(
height: 36,
child: Center(child: CupertinoActivityIndicator()));
}
})
這樣兜起來,我們就成功的實現無限捲軸的邏輯!不過都是同樣的新聞顯示效果看了有點了無新意XDD 所以各位可以參考我們的設計稿來實作看看不一樣的新聞卡片。這邊也就交給各位讀者自行實作拉。以下是我的最終結果:
簡單總結今天的內容:
ScrollController
是一個用於控制滾動或監聽滾動進度的控制器,專用於滾動式儲組件上CustomScrollView
用於實現自定義的滾動效果,在其中的組件需為 sliver
類別組件無限捲軸早就不是一個新概念,有一個很棒的套件名為 infinite_scroll_pagination
廣受開發者的喜愛,只要套用就可以很快的實現無限捲軸的效果。如果有興趣的可以到這裡看看他們提供的文件,寫的蠻完整的。不過透過今天的介紹,靠著簡單的邏輯思考同樣也實現出不錯的效果,相信一定比起套套件更有成就感!
今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day19/micro_news_app